/*
* Copyright 2013 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.apps.dashclock;
import com.google.android.apps.dashclock.render.DashClockRenderer;
import com.google.android.apps.dashclock.render.SimpleRenderer;
import com.google.android.apps.dashclock.render.SimpleViewBuilder;
import net.nurik.roman.dashclock.R;
import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.os.Handler;
import android.preference.PreferenceManager;
import android.service.dreams.DreamService;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.Property;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.animation.LinearInterpolator;
import android.widget.FrameLayout;
import android.widget.ScrollView;
import java.util.List;
import static com.google.android.apps.dashclock.ExtensionManager.ExtensionWithData;
/**
* Daydream for DashClock.
*/
public class DaydreamService extends DreamService implements
ExtensionManager.OnChangeListener,
DashClockRenderer.OnClickListener {
private static final String PREF_DAYDREAM_COLOR = "pref_daydream_color";
private static final int DEFAULT_FOREGROUND_COLOR = 0xffffffff;
private static final int CYCLE_INTERVAL_MILLIS = 20000;
private static final int FADE_MILLIS = 5000;
private static final int TRAVEL_ROTATE_DEGREES = 3;
private Handler mHandler = new Handler();
private ExtensionManager mExtensionManager;
private int mTravelDistance;
private int mForegroundColor;
private ViewGroup mDaydreamContainer;
private ViewGroup mExtensionsContainer;
private AnimatorSet mSingleCycleAnimator;
private boolean mAttached;
private boolean mNeedsRelayout;
private boolean mMovingLeft;
@Override
public void onDreamingStarted() {
super.onDreamingStarted();
mExtensionManager = ExtensionManager.getInstance(this);
mExtensionManager.addOnChangeListener(this);
}
@Override
public void onDreamingStopped() {
super.onDreamingStopped();
mExtensionManager.removeOnChangeListener(this);
mExtensionManager = null;
}
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
mAttached = true;
setInteractive(true);
setFullscreen(false);
setScreenBright(false);
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
mForegroundColor = sp.getInt(PREF_DAYDREAM_COLOR, DEFAULT_FOREGROUND_COLOR);
Resources res = getResources();
mTravelDistance = res.getDimensionPixelSize(R.dimen.daydream_travel_distance);
layoutDream();
}
@Override
public void onDetachedFromWindow() {
super.onDetachedFromWindow();
mHandler.removeCallbacksAndMessages(null);
mAttached = false;
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
mHandler.removeCallbacks(mCycleRunnable);
layoutDream();
}
@Override
public void onExtensionsChanged() {
mHandler.removeCallbacks(mHandleExtensionsChanged);
mHandler.postDelayed(mHandleExtensionsChanged,
ExtensionHost.UPDATE_COLLAPSE_TIME_MILLIS);
}
private Runnable mHandleExtensionsChanged = new Runnable() {
@Override
public void run() {
renderDaydream(false);
}
};
private void layoutDream() {
setContentView(R.layout.daydream);
mNeedsRelayout = true;
renderDaydream(true);
mHandler.removeCallbacks(mCycleRunnable);
mHandler.postDelayed(mCycleRunnable, CYCLE_INTERVAL_MILLIS - FADE_MILLIS);
}
private void renderDaydream(final boolean restartAnimation) {
if (!mAttached) {
return;
}
final Resources res = getResources();
mDaydreamContainer = (ViewGroup) findViewById(R.id.daydream_container);
TouchToAwakeFrameLayout awakeContainer = (TouchToAwakeFrameLayout)
findViewById(R.id.touch_awake_container);
awakeContainer.setOnAwakeListener(new TouchToAwakeFrameLayout.OnAwakeListener() {
@Override
public void onAwake() {
mHandler.removeCallbacks(mCycleRunnable);
mHandler.postDelayed(mCycleRunnable, CYCLE_INTERVAL_MILLIS);
mDaydreamContainer.animate()
.alpha(1f)
.rotation(0)
.scaleX(1f)
.scaleY(1f)
.translationX(0f)
.translationY(0f)
.setDuration(res.getInteger(android.R.integer.config_shortAnimTime));
mSingleCycleAnimator.cancel();
}
});
DisplayMetrics displayMetrics = res.getDisplayMetrics();
int screenHeightDp = (int) (displayMetrics.heightPixels * 1f / displayMetrics.density);
// Set up rendering
SimpleRenderer renderer = new SimpleRenderer(this);
DashClockRenderer.Options options = new DashClockRenderer.Options();
options.target = DashClockRenderer.Options.TARGET_DAYDREAM;
options.minHeightDp = screenHeightDp;
options.newTaskOnClick = true;
options.onClickListener = this;
options.clickIntentTemplate = WidgetClickProxyActivity.getTemplate(this);
renderer.setOptions(options);
// Render the clock face
SimpleViewBuilder vb = renderer.createSimpleViewBuilder();
vb.useRoot(mDaydreamContainer);
renderer.renderClockFace(vb);
vb.setLinearLayoutGravity(R.id.clock_target, Gravity.CENTER_HORIZONTAL);
// Render extensions
mExtensionsContainer = (ViewGroup) findViewById(R.id.extensions_container);
mExtensionsContainer.removeAllViews();
List<ExtensionWithData> visibleExtensions
= ExtensionManager.getInstance(this).getVisibleExtensionsWithData();
for (ExtensionWithData ewd : visibleExtensions) {
mExtensionsContainer.addView(
(View) renderer.renderExpandedExtension(mExtensionsContainer, null, ewd));
}
if (mDaydreamContainer.getHeight() == 0 || mNeedsRelayout) {
ViewTreeObserver vto = mDaydreamContainer.getViewTreeObserver();
if (vto.isAlive()) {
vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
ViewTreeObserver vto = mDaydreamContainer.getViewTreeObserver();
if (vto.isAlive()) {
vto.removeOnGlobalLayoutListener(this);
}
postLayoutRender(restartAnimation);
}
});
}
mDaydreamContainer.requestLayout();
mNeedsRelayout = false;
} else {
postLayoutRender(restartAnimation);
}
}
/**
* Post-layout render code.
*/
public void postLayoutRender(boolean restartAnimation) {
Resources res = getResources();
// Adjust the ScrollView
ExposedScrollView scrollView = (ExposedScrollView) findViewById(R.id.extensions_scroller);
int maxScroll = scrollView.computeVerticalScrollRange() - scrollView.getHeight();
if (maxScroll < 0) {
ViewGroup.LayoutParams lp = scrollView.getLayoutParams();
lp.height = mExtensionsContainer.getHeight();
scrollView.setLayoutParams(lp);
mDaydreamContainer.requestLayout();
}
// Recolor widget
Utils.traverseAndRecolor(mDaydreamContainer, mForegroundColor, true);
if (restartAnimation) {
int x = (mMovingLeft ? 1 : -1) * mTravelDistance;
int deg = (mMovingLeft ? 1 : -1) * TRAVEL_ROTATE_DEGREES;
mMovingLeft = !mMovingLeft;
mDaydreamContainer.animate().cancel();
mDaydreamContainer.setScaleX(0.85f);
mDaydreamContainer.setScaleY(0.85f);
if (mSingleCycleAnimator != null) {
mSingleCycleAnimator.cancel();
}
Animator scrollDownAnimator = ObjectAnimator.ofInt(scrollView,
ExposedScrollView.SCROLL_POS, 0, maxScroll);
scrollDownAnimator.setDuration(CYCLE_INTERVAL_MILLIS / 5);
scrollDownAnimator.setStartDelay(CYCLE_INTERVAL_MILLIS / 5);
Animator scrollUpAnimator = ObjectAnimator.ofInt(scrollView,
ExposedScrollView.SCROLL_POS, 0);
scrollUpAnimator.setDuration(CYCLE_INTERVAL_MILLIS / 5);
scrollUpAnimator.setStartDelay(CYCLE_INTERVAL_MILLIS / 5);
AnimatorSet scrollAnimator = new AnimatorSet();
scrollAnimator.playSequentially(scrollDownAnimator, scrollUpAnimator);
Animator moveAnimator = ObjectAnimator.ofFloat(mDaydreamContainer,
View.TRANSLATION_X, x, -x).setDuration(CYCLE_INTERVAL_MILLIS);
moveAnimator.setInterpolator(new LinearInterpolator());
Animator rotateAnimator = ObjectAnimator.ofFloat(mDaydreamContainer,
View.ROTATION, deg, -deg).setDuration(CYCLE_INTERVAL_MILLIS);
moveAnimator.setInterpolator(new LinearInterpolator());
mSingleCycleAnimator = new AnimatorSet();
mSingleCycleAnimator.playTogether(scrollAnimator, moveAnimator, rotateAnimator);
mSingleCycleAnimator.start();
}
}
public Runnable mCycleRunnable = new Runnable() {
@Override
public void run() {
mDaydreamContainer.animate().alpha(0f).setDuration(FADE_MILLIS)
.withEndAction(new Runnable() {
@Override
public void run() {
renderDaydream(true);
mHandler.removeCallbacks(mCycleRunnable);
mHandler.postDelayed(mCycleRunnable,
CYCLE_INTERVAL_MILLIS - FADE_MILLIS);
mDaydreamContainer.animate().alpha(1f).setDuration(FADE_MILLIS);
}
});
}
};
@Override
public void onClick() {
// Any time anything in DashClock is clicked
finish();
}
/**
* FrameLayout that can notify listeners of ACTION_DOWN events.
*/
public static class TouchToAwakeFrameLayout extends FrameLayout {
private OnAwakeListener mOnAwakeListener;
public TouchToAwakeFrameLayout(Context context) {
super(context);
}
public TouchToAwakeFrameLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public TouchToAwakeFrameLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
if (mOnAwakeListener != null) {
mOnAwakeListener.onAwake();
}
break;
// ACTION_UP doesn't seem to reliably get called. Otherwise
// should postDelayed on ACTION_UP instead of ACTION_DOWN.
}
return false;
}
public void setOnAwakeListener(OnAwakeListener onAwakeListener) {
mOnAwakeListener = onAwakeListener;
}
public static interface OnAwakeListener {
void onAwake();
}
}
/**
* ScrollView that exposes its scroll range.
*/
public static class ExposedScrollView extends ScrollView {
public ExposedScrollView(Context context) {
super(context);
}
public ExposedScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ExposedScrollView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
public int computeVerticalScrollRange() {
return super.computeVerticalScrollRange();
}
public static final Property<ScrollView, Integer> SCROLL_POS
= new Property<ScrollView, Integer>(Integer.class, "scrollPos") {
@Override
public void set(ScrollView object, Integer value) {
object.scrollTo(0, value);
}
@Override
public Integer get(ScrollView object) {
return object.getScrollY();
}
};
}
}